#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#include "cpu.h"


/*
*   Constructs the cpu object
*/
CPUPtr cpuConstructor(int numComputeBound, int numProducerConsumer, void(*notifyObserver)(PacketPtr)) {
        CPUPtr toReturn = (CPUPtr) malloc(sizeof(CPUStr));
        toReturn->allPCB = queueConstructor(); //store all pcb's and processes in here for easy destruction at the end. (call destructAll on the queue)
        toReturn->ioDevices = (IODevicePtr*) malloc(sizeof(IODevicePtr) * NUM_IO);
        toReturn->numProducerConsumer = numProducerConsumer;
        toReturn->mutexes = (MutexPtr*) malloc(sizeof(MutexPtr) * numProducerConsumer);  //each producer consumer pair gets a mutex
        toReturn->numCVs = numProducerConsumer * 2;//each producer consumer pair gets two cond var.
        toReturn->cvs = (CVPtr*) malloc(sizeof(CVStr) * toReturn->numCVs);
        toReturn->threads = (pthread_t*) malloc(sizeof(pthread_t) * (NUM_IO + 1)); //IO, TIMER
        toReturn->readyQueue = queueConstructor();     //the ready queue
        toReturn->step_no = 0; //the current count
        toReturn->isRunning = TRUE;  //the running state of the cpu

        pthread_mutex_init(&(toReturn->mutexRQ), NULL);//initialize ready queue's mutex

        //function pointers
        toReturn->destruct = cpuDestructor;
        toReturn->step = cpuStep;
        toReturn->scheduler = cpuRRScheduler;  //set what type of scheduling algorithm to run.
        toReturn->notifyObserver = notifyObserver;

        toReturn->notifyObserver((PacketPtr)NULL);
        //initialize keyboard process & put into running block
        srand(time(NULL));

        int i = 0;  //initialize sharedMemory
        toReturn->sharedMemory = (int*) malloc(sizeof(int) * numProducerConsumer); //a shared memory slot for each producer consumer pair
        for(; i < numProducerConsumer; i++) {
            toReturn->sharedMemory[i] = NO_ID;
        }


        i = 0;
        for(; i < numComputeBound; i++) { //initialize computeBound processes & put in ready queue
            RequestPtr* req = (RequestPtr*) malloc(sizeof(RequestPtr)); //the requests to run
            req[0] = requestConstructor(NOTHING, 0);            //a single nothing request for compute bound
            ProcessPtr proc = processConstructor(COMPUTE, 1, 1, req);
            PCBPtr pcb = pcbConstructor(i, proc);
            toReturn->allPCB->enqueue(toReturn->allPCB, pcb);
            toReturn->readyQueue->enqueue(toReturn->readyQueue,pcb);
        }


        //2 io devices & 2 io processes
        //generate random number for number of requests
        //create requests (number = to random that was generated)
        //create an array of 2 io blocking queues
        pthread_attr_t attr;
        pthread_attr_init(&attr);
        pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
        pthread_attr_setschedpolicy(&attr, SCHED_RR);

        int t = 0;
        for(; t < NUM_IO-1; t++) {  //-1 to exclude the keyboard
            int numSteps = (rand() % STEP_MOD) + 1; //set to random number, (+1 to avoid floating point exception)
            if(numSteps < STEP_MOD/4)   //prevent a process from running to quickly
                numSteps += STEP_MOD/4;
            int numRequests = 1;  //io devices only have one request
            toReturn->ioDevices[t] = ioConstructor(t);  //create io device w/ all it's STUFF
            RequestPtr* req = (RequestPtr*) malloc(sizeof(RequestPtr)); //the requests to run
            req[0] = requestConstructor(IO_REQUEST, t);
            ProcessPtr ioproc = processConstructor(IOBOUND, numSteps, numRequests, req);
            PCBPtr iopcb = pcbConstructor(i+t, ioproc); //i+j for the current process id
            toReturn->allPCB->enqueue(toReturn->allPCB, iopcb);    //enqueue for easy destruction at end of program
            toReturn->readyQueue->enqueue(toReturn->readyQueue,iopcb);  //put in ready queue

            pthread_create(&(toReturn->threads[t]), &attr, IODevice, (void *)toReturn); //start the io device
        }

            //create the keyboard process  & store with the io device processes
        toReturn->ioDevices[t] = ioConstructor(t);  //create io device w/ all it's STUFF
        RequestPtr* req = (RequestPtr*) malloc(sizeof(RequestPtr)); //the requests to run
        req[0] = requestConstructor(KEYBOARD_REQUEST, t);
        ProcessPtr kbProc = processConstructor(KEYBOARD, 1, 1, req);
        PCBPtr kbPCB = pcbConstructor(i+t, kbProc); //i+j for the current process id
        toReturn->allPCB->enqueue(toReturn->allPCB, kbPCB);    //enqueue for easy destruction at end of program
        toReturn->readyQueue->enqueue(toReturn->readyQueue,kbPCB);  //put in ready queue

        pthread_create(&(toReturn->threads[t]), &attr, KeyboardDevice, (void *)toReturn); //start the io device
        t++; //increment so the future processes will have the correct index



        //create producer processes
        int k = 0;
        for(; k < numProducerConsumer; k++) {
            int numSteps = (rand() % STEP_MOD) + 1; //set to random number, (+1 to avoid floating point exception)
            if(numSteps < STEP_MOD/2)   //prevent a process from running to quickly
                numSteps += STEP_MOD/4;
            int numRequests = 7;
            RequestPtr* req = (RequestPtr*) malloc(sizeof(RequestPtr) * numRequests); //holds a pseudo program to run
            //asdf
            //lock mutex
            req[0] = requestConstructor(LOCK_MTX, k);
            //if bufavail == 0
                //wait on buf not full
            req[1] = requestCVConstructor(CHECK_FULL, 2*k, k);
            //unlock mutex
            req[2] = requestConstructor(UNLOCK_MTX, k);
            //write to memory
            req[3] = requestConstructor(WRITE, k);
            //lock mutex
            req[4] = requestConstructor(LOCK_MTX, k);
            //signal buf not empty
            req[5] = requestCVConstructor(SIGNAL, 2*k + 1, k);
            //unlock
            req[6] = requestConstructor(UNLOCK_MTX, k);

            toReturn->mutexes[k] = mutexConstructor();              //mutex that is shared by producer & consumer
            toReturn->cvs[2*k] = conditionVariableConstructor();   //buff_not_full
            toReturn->cvs[2*k + 1] = conditionVariableConstructor();  //buff_not_empty
            ProcessPtr prodProc = processConstructor(PRODUCER, numSteps, numRequests, req);
            PCBPtr prodPCB = pcbConstructor(i+t+k, prodProc); //i+j for the current process id
            toReturn->allPCB->enqueue(toReturn->allPCB, prodPCB);    //enqueue for easy destruction at end of program
            toReturn->readyQueue->enqueue(toReturn->readyQueue,prodPCB);  //put in ready queue
        }

        //create consumer processes
        int c = 0;
        for(; c < numProducerConsumer; c++) {
            int numSteps = (rand() % STEP_MOD) + 1; //set to random number, (+1 to avoid floating point exception)
            if(numSteps < STEP_MOD/2)   //prevent a process from running to quickly
                numSteps += STEP_MOD/4;
            int numRequests = 7;
            RequestPtr* req = (RequestPtr*) malloc(sizeof(RequestPtr) * numRequests); //holds a pseudo program to run

            //lock mutex
            req[0] = requestConstructor(LOCK_MTX, c);
            //if nothing in shared memory
                //wait on buf not empty
            req[1] = requestCVConstructor(CHECK_EMPTY, c*2 + 1, c);
            //unlock mutex
            req[2] = requestConstructor(UNLOCK_MTX, c);
            //read memory
            req[3] = requestConstructor(READ, c);
            //lock mutex
            req[4] = requestConstructor(LOCK_MTX, c);
            //signal buf not full
            req[5] = requestCVConstructor(SIGNAL, c*2, c);
            //unlock mutex
            req[6] = requestConstructor(UNLOCK_MTX, c);

            ProcessPtr consProc = processConstructor(CONSUMER, numSteps, numRequests, req);
            PCBPtr consPCB = pcbConstructor(i+t+k+c, consProc); //i+j for the current process id
            toReturn->allPCB->enqueue(toReturn->allPCB, consPCB);    //enqueue for easy destruction at end of program
            toReturn->readyQueue->enqueue(toReturn->readyQueue,consPCB);  //put in ready queue
        }



        toReturn->running = toReturn->readyQueue->dequeue(toReturn->readyQueue);      //the currently running process, set to the first process created

        //initialize Producer Consumer

    //create timer thread
    pthread_setschedprio((toReturn->threads[t]), 1);
    pthread_create(&(toReturn->threads[t++]), &attr, Timer, (void *)toReturn);


    pthread_attr_destroy(&attr);  //don't need the attribute anymore
    return toReturn;
}

void cpuDestructor(CPUPtr this) {
    this->allPCB->destructAll(this->allPCB); //destroys everything contained in the queue
    this->readyQueue->destruct(this->readyQueue);
    int i = 0;
    for(; i < NUM_IO; i++) {  //destroy io devices.
       ioDestructor(this->ioDevices[i]);
    }
    free(this->ioDevices);

    i = 0;
    for(; i < this->numProducerConsumer; i++) { //destroy mutexes.
        mutexDestructor(this->mutexes[i]);
    }
    free(this->mutexes);

    i = 0;
    for(; i < this->numProducerConsumer; i++) { //destroy condition variables.
        this->cvs[i]->destruct(this->cvs[i]);
    }
    free(this->cvs);
    free(this->sharedMemory);
    free(this->threads);
    free(this);
}

////////////////////////////////INTERRUPT GENERATING THREADS //////////////////////////////////

/*
*   this is a CPUPtr
*/
void *Timer(void *this) {
    while(((CPUPtr)this)->isRunning) {
        sleep(WAIT_TIME);//thread sleep
        cpuTimer_int_handler((CPUPtr) this);
    }
    pthread_exit(NULL);
}

//send the io device as a parameter
/*
*  CPUPtr is the parameter
*/
void *IODevice(void *this) {
    static int id = 0;  //this lets the function know which io device to run
    int thisID = id;    //store the value because the next io device will increment the static value
    id++;                   //increment for the next device.
    while(((CPUPtr)this)->isRunning) {
        //wait until there is something in this device's io queue
        pthread_mutex_lock(&(((CPUPtr)this)->ioDevices[thisID]->ioMTX));//lock mutex, otherwise behavior is undefined
        pthread_cond_wait(&(((CPUPtr)this)->ioDevices[thisID]->ioCV), &(((CPUPtr)this)->ioDevices[thisID]->ioMTX));
        if(((CPUPtr)this)->isRunning) { //cpu not be running if thread was waiting after simulator loop finished
            sleep(rand() % SLEEP_MOD); //sleep for a random amount of time, then service the request.
            io_int_handler((CPUPtr)this, thisID);//service the waiting process

        }
        pthread_mutex_unlock(&(((CPUPtr)this)->ioDevices[thisID]->ioMTX));//release mutex
    }
    pthread_exit(NULL);
}

void *KeyboardDevice(void *this) {
    while(((CPUPtr)this)->isRunning) {
        pthread_mutex_lock(&(((CPUPtr)this)->ioDevices[NUM_IO-1]->ioMTX));//lock mutex, otherwise behavior is undefined
        pthread_cond_wait(&(((CPUPtr)this)->ioDevices[NUM_IO-1]->ioCV), &(((CPUPtr)this)->ioDevices[NUM_IO-1]->ioMTX));
        if(((CPUPtr)this)->isRunning) {
            getchar();//get the character
            getchar();//remove the newline from the buffer
            io_int_handler((CPUPtr)this, NUM_IO-1);//service the waiting process
        }
        pthread_mutex_unlock(&(((CPUPtr)this)->ioDevices[NUM_IO-1]->ioMTX));//release mutex
    }
    pthread_exit(NULL);

}

////////////////////////////////KERNAL HANDLERS/////////////////////



/*
*   sets the currently running process
*   ASSUME: the mutex is held by caller
*/
int cpuRRScheduler(CPUPtr this) {
    if (this->running == (PCBPtr) NULL) {
        this->running = this->readyQueue->dequeue(this->readyQueue);
    }
    return NO_ERROR;
}

/*
*   Handles an io interrupt
*   Param ioID: the id of the io device that sent the interrupt
*/
int io_int_handler(CPUPtr this, int ioID) {
     pthread_mutex_lock(&this->mutexRQ);//lock mutex
     PCBPtr pcb = this->ioDevices[ioID]->ioQueue->dequeue(this->ioDevices[ioID]->ioQueue);
     this->readyQueue->enqueue(this->readyQueue, pcb);
     PacketPtr packet = packetConstructor(IO_INT, ioID, this->running->pid, 0, this->readyQueue, this->ioDevices, this->mutexes, this->cvs, this->numProducerConsumer);
     pthread_mutex_unlock(&this->mutexRQ);//unlock mutex
     this->notifyObserver(packet);
}

/*
*   Moves the requesting pcb into the io blocking queue & activates the io device
*/
PacketPtr io_req_trap_handler(CPUPtr this, RequestPtr request) {
     pthread_mutex_lock(&this->mutexRQ);//lock readyQueue mutex since we are changing the running process
     pthread_mutex_lock(&(this->ioDevices[request->objectID]->ioMTX));//lock io mutex to avoid problems if the io int handler is running
     PCBPtr pcb = this->running;
     this->running = (PCBPtr) NULL;
     this->ioDevices[request->objectID]->ioQueue->enqueue(this->ioDevices[request->objectID]->ioQueue, pcb); //put in blocking queue
     this->scheduler(this); //call the scheduler to run the next pcb in the queue
     PacketPtr packet = packetConstructor(IO_REQUEST, request->objectID, this->running->pid, 0, this->readyQueue, this->ioDevices, this->mutexes, this->cvs, this->numProducerConsumer);
     pthread_mutex_unlock(&this->mutexRQ);//unlock mutex
     pthread_mutex_unlock(&(this->ioDevices[request->objectID]->ioMTX));//unlock mutex
     pthread_cond_signal(&(this->ioDevices[request->objectID]->ioCV));//signal waiting to wake io device
     return packet;
}


int cpuTimer_int_handler(CPUPtr this) {
    if(this->running != (PCBPtr) NULL) {  //If it is null then do nothing, everything is blocked.
        pthread_mutex_lock(&this->mutexRQ);   //get the mutex
        this->readyQueue->enqueue(this->readyQueue, this->running);  //adjust ready queue
        this->running = (PCBPtr) NULL;
        this->scheduler(this); //call scheduler
        PacketPtr packet = packetConstructor(TIMER_INT, NO_ID, this->running->pid, 0, this->readyQueue, this->ioDevices, this->mutexes, this->cvs, this->numProducerConsumer);
        pthread_mutex_unlock(&this->mutexRQ);//release mutex
        this->notifyObserver(packet);
    }
}

PacketPtr mutex_lock_trap_handler(CPUPtr this, RequestPtr request) {
    pthread_mutex_lock(&this->mutexRQ);  //lock readyQueue mutex
    //if owner == null then set the running process to the owner & continue with the timeslice
    if(this->mutexes[request->objectID]->owner == NO_ID) //if there is currently no owner
    {
        //set the owner to the currently running process
        this->mutexes[request->objectID]->owner = this->running->pid;
    } else {
        //block current process & add to the mutex queue
        this->mutexes[request->objectID]->processQueue->enqueue(this->mutexes[request->objectID]->processQueue,this->running);
        this->running = (PCBPtr) NULL; //set running process block to null
        this->scheduler(this); //call scheduler
    }
    PacketPtr packet = packetConstructor(LOCK_MTX, request->objectID, this->running->pid, 0, this->readyQueue, this->ioDevices, this->mutexes, this->cvs, this->numProducerConsumer);
    pthread_mutex_unlock(&this->mutexRQ);//release mutex
    return packet;
}


PacketPtr mutex_unlock_trap_handler(CPUPtr this, RequestPtr request) {
    pthread_mutex_lock(&this->mutexRQ);  //lock rq mutex
    this->mutexes[request->objectID]->owner = NO_ID; //take owner pcb out of owner block
    PCBPtr pcb = this->mutexes[request->objectID]->processQueue->dequeue(this->mutexes[request->objectID]->processQueue);
    //dequeue the mutex queuee & set owner block to the pcb we received from the dequeue
    if(pcb != (PCBPtr)NULL) {//add the pcb we received to the ready queue
        this->mutexes[request->objectID]->owner = pcb->pid;
        this->readyQueue->enqueue(this->readyQueue, pcb);
    }
    PacketPtr packet = packetConstructor(UNLOCK_MTX, request->objectID, this->running->pid, 0, this->readyQueue, this->ioDevices, this->mutexes, this->cvs, this->numProducerConsumer);
    pthread_mutex_unlock(&this->mutexRQ);//release mutex
    return packet;
}

/*
*   add the currently running process to the request condition variable's waiting queue
*   The mutex that the process currently holds is released, and the next process in the mutexes
*   blocking queue gets control of the mutex.
*/
PacketPtr cond_wait_trap_handler(CPUPtr this, RequestPtr request) {
         //enqueue the running process & requested mutex to the condion variable's queue
    pthread_mutex_lock(&this->mutexRQ);  //lock rq mutex
    queueConditionEnqueue(this->cvs[request->objectID]->waitingQueue, this->running, request->mutexID); //put running process in condition variable's queue
    this->running = (PCBPtr) NULL; //set running block to null so scheduler can start the next process

        //unlock the mutex that's associated with this request
    PCBPtr pcb = this->mutexes[request->mutexID]->processQueue->dequeue(this->mutexes[request->mutexID]->processQueue);
    if(pcb != (PCBPtr)NULL) {//add the pcb we received to the ready queue
        this->mutexes[request->mutexID]->owner = pcb->pid;
        this->readyQueue->enqueue(this->readyQueue, pcb);
    } else {
        this->mutexes[request->mutexID]->owner = NO_ID; //mutex has no owner
    }
    this->scheduler(this); //call scheduler
        //assemble packet
    PacketPtr packet = packetConstructor(WAIT, request->objectID, this->running->pid, 0, this->readyQueue, this->ioDevices, this->mutexes, this->cvs, this->numProducerConsumer);
    pthread_mutex_unlock(&this->mutexRQ);  //unlock rq mutex
    return packet;
}

PacketPtr cond_signal_trap_handler(CPUPtr this, RequestPtr request) {
    pthread_mutex_lock(&this->mutexRQ);  //lock rq mutex
            //dequeue the front of the conditional variable's waiting queue
    NodePtr mtxNpcb = queueConditionDequeue(this->cvs[request->objectID]->waitingQueue);

    if(mtxNpcb != (NodePtr) NULL) {  //try to get a lock on the mutex associated with the request
        int mutexID = mtxNpcb->mutexID;
        PCBPtr pcb = mtxNpcb->contents;
        if(this->mutexes[mutexID]->owner == NO_ID) //if there is currently no owner
        {
            //set the owner to the currently running process
            this->mutexes[mutexID]->owner = pcb->pid;  //set mutex owner to this process
            this->readyQueue->enqueue(this->readyQueue, pcb);  //put the pcb back on the ready queue
        } else {
                //else if something already owns the mutex, then put this process at the end of the mutexes' queue
            this->mutexes[mutexID]->processQueue->enqueue(this->mutexes[mutexID]->processQueue, pcb);
        }
    }
    PacketPtr packet = packetConstructor(WAIT, request->objectID, this->running->pid, 0, this->readyQueue, this->ioDevices, this->mutexes, this->cvs, this->numProducerConsumer);
    pthread_mutex_unlock(&this->mutexRQ);  //unlock rq mutex
    return packet;
}

/////////////////////////////////////USER SPACE FUNCTIONS////////////////////////////////

/*
*   User space function that calls kernal space function
*/
PacketPtr io_system_call(CPUPtr this, RequestPtr request) {
    return io_req_trap_handler(this, request);
}

PacketPtr mutex_lock_system_call(CPUPtr this, RequestPtr request) {
    return mutex_lock_trap_handler(this, request);
}

PacketPtr mutex_unlock_system_call(CPUPtr this, RequestPtr request) {
    return mutex_unlock_trap_handler(this, request);
}

PacketPtr cond_wait_system_call(CPUPtr this, RequestPtr request) {
    return cond_wait_trap_handler(this, request);
}

PacketPtr cond_signal_system_call(CPUPtr this, RequestPtr request) {
    return cond_signal_trap_handler(this, request);
}

/*
*   Write a value to a shared memory location
*   always return a null PacketPtr
*/
PacketPtr write_shared_memory(CPUPtr this, RequestPtr request) {
    static int value = 0;
    this->sharedMemory[request->objectID] = value++;
    return (PacketPtr)NULL;
}


/*
*   reads a value from shared memory and returns the value included in the packet
*/
PacketPtr read_shared_memory(CPUPtr this, RequestPtr request) {
    PacketPtr packet = packetConstructor(WAIT, request->objectID, this->running->pid, 0, this->readyQueue, this->ioDevices, this->mutexes, this->cvs, this->numProducerConsumer);
    packet->consumersValue = this->sharedMemory[request->objectID];
    this->sharedMemory[request->objectID] = NO_ID;
    return packet;
}

/*
    simulates block of code from sample program #5
    if(buffAvail == 0)
        condition_wait(buf_not_full, buflock)

*/
PacketPtr check_shared_memory_full(CPUPtr this, RequestPtr request) {
    if(this->sharedMemory[request->mutexID] != NO_ID) { //shared memory is full, wait on a condition
        return cond_wait_system_call(this, request);
    } else {  //there is room in memory
        return (PacketPtr) NULL;
    }

}

/*
    simulates block of code from sample program #5
    if(buffAvail == MAX)
        condition_wait(buf_not_empty, buflock)

*/
PacketPtr check_shared_memory_empty(CPUPtr this, RequestPtr request) {
    if(this->sharedMemory[request->mutexID] == NO_ID) { //shared memory is empty, wait on a condition
        return cond_wait_system_call(this, request);
    } else {  //there is something stored in memory
        return (PacketPtr) NULL;
    }

}

///////////////////////////////////HELPER FUNCTIONS/////////////////////////////////


/*
*   run a single step, the simulator handles the loop
*   return a packet containing the information the gui needs to update properly.
*   The packet will need to be freed from memory by the caller of this function.
*/
PacketPtr cpuStep(CPUPtr this) {
    PacketPtr packet = (PacketPtr) NULL;
    if(this->running != (PCBPtr) NULL) {  //our version of an idl process
        RequestPtr req = this->running->step(this->running);     //call step on the current process
        packet = handleRequest(this, req);
    }
    return packet;
}

/*
*   delegates a request to the correct function that will handle the request.
*/
PacketPtr handleRequest(CPUPtr this, RequestPtr request) {
    PacketPtr toReturn = (PacketPtr) NULL;
    if(request != (RequestPtr)NULL) {       //a null request means the process is in between requests
        switch(request->requestType) {
            case NOTHING:       /*do nothing*/     break;
            case IO_REQUEST:     toReturn = io_system_call(this, request); break;
            case KEYBOARD_REQUEST: toReturn = io_system_call(this, request); break;
            case LOCK_MTX:       toReturn = mutex_lock_system_call(this, request); break;
            case UNLOCK_MTX:     toReturn = mutex_unlock_system_call(this, request); break;
            case WAIT:           toReturn = cond_wait_system_call(this, request); break;
            case SIGNAL:         toReturn = cond_signal_system_call(this, request); break;
            case READ:           toReturn = read_shared_memory(this, request); break;
            case WRITE:          toReturn = write_shared_memory(this, request); break;
            case CHECK_FULL:     toReturn = check_shared_memory_full(this, request); break;
            case CHECK_EMPTY:    toReturn = check_shared_memory_empty(this, request); break;
            default:             printf("OH NO! default hit in handleRequest!\n"); break;
        }
    }
    return toReturn;
}


/*
*   Wake all of the io threads so the program can terminate
*/
void wakeIOThreads(CPUPtr this) {
    int i = 0;
    for(;i < NUM_IO; i++) {
        pthread_cond_signal(&(((CPUPtr)this)->ioDevices[i]->ioCV));
    }
}

/*
int main() {
    CPUPtr cpu = cpuConstructor(5,5,5);
    sleep(5);
    //thread join
    //then call destructor
    //then exit
    pthread_exit(NULL);
    printf("waiting for done\n");
    cpu->destruct(cpu);
    printf("done\n");

}*/
